package Chat;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.net.MalformedURLException;
import java.rmi.*;
import javax.swing.JMenuItem;
import static javax.swing.JOptionPane.*;
import javax.swing.JTabbedPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;


import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

/**
 *
 * @author danielsl & thorerikmidtb
 * This is the client which gets the reference to the remote object and invokes its methods
 */
class Client {

    private Server server;
    private Callback callback;
    private int connectionState;
    private GUI gui;
    private ArrayList<GUI.PMchatWindow> pmChatList;
    private int selectedTab;
    private ArrayList<String> users;


    /**
     * Constructor
     */
    public Client() {
        try {
            initComponents();
        } catch (RemoteException ex) {
            Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    public void initComponents() throws RemoteException {
        gui = new GUI();
        connectionState = 0;
        pmChatList = new ArrayList<GUI.PMchatWindow>();
        users = new ArrayList<String>();
        selectedTab = 0;


        /* Adding all of the menu items to the actionListener and giving each of
         * them action commands. The action commands are integers in String-form
         * so the parsing will be easier in actionPerformed().
         */

        ThemeActionListener al = new ThemeActionListener();
        MenuActionListener el = new MenuActionListener();
        ActionUserList ul = new ActionUserList();
        ChangeTabListener ctl = new ChangeTabListener();

        /*Adding themes to their action listener */
        gui.getDefaultTheme().addActionListener(al);
        gui.getDefaultTheme().setActionCommand("1");

        gui.getMatrixTheme().addActionListener(al);
        gui.getMatrixTheme().setActionCommand("2");

        gui.getGirlyTheme().addActionListener(al);
        gui.getGirlyTheme().setActionCommand("3");

        /*Adding menu items to the action listener */

        gui.getConnectMenuItem().addActionListener(el);
        gui.getQuitMenuItem().addActionListener(el);

        /* Adding userlist items to the action listener */
        gui.getUserList().addMouseListener(ul);

        gui.getTabbedMainChat().addChangeListener(ctl);

        /* Adding the frame to its action listener */

        gui.addWindowListener(new WindowAdapter() {

            public void windowClosing(WindowEvent we) {
                if (connectionState == 1) {
                    try {
                        server.quit(callback);
                    } catch (Exception e) {
                        System.out.println("OW, something went wrong :(  " + e);
                    }
                }
            }
        });

        /*
         * This is the listener for the userlist. It will help to create right tab
         * for the selected user.
         */
        UserListSelectionListener listus = new UserListSelectionListener();
        gui.getUserList().addListSelectionListener(listus);

        gui.getInputText().addKeyListener(new java.awt.event.KeyAdapter() {

            public void keyPressed(java.awt.event.KeyEvent evt) {
                inputTextKeyPressed(evt);
            }
        });


    }

    /**
     * Method for the connect button, if the connection state is set to 0, it means that the program is not connected.
     * Pushing the button will then try to connect to the server
     * You will then have to type in a user name and you are ready to chat
     * If the connection state is set to 1 however, the connect button is then showed as a disconnect button.
     */
    private void connectClicked() {
        try {
            server = (Server) Naming.lookup("//127.0.0.1/ServerImpl");
            callback = new CallbackImpl();
            String userName = showInputDialog("Please input a user name");
            callback.setUserName(userName);
            callback.setTextArea(gui.getTextAreaMainChat());
            selectedTab = -1;
            server.connect(callback);
            connectionState = 1;
            gui.getConnectMenuItem().setText("Disconnect");
            gui.getInputText().requestFocusInWindow();
        } catch (NotBoundException bound) {
            System.out.println("Error: Not bound exception\n" + bound);
        } catch (MalformedURLException url) {
            System.out.println("Error: No RMI register located on" + server + "\nErrormessage" + url);
        } catch (RemoteException re) {
            gui.getTextAreaMainChat().setText("Error: Cannot establish a connection to the server.\n");
            System.out.println("Error: Something went wrong remotely\n" + re);
        }
    }

    /**
     * Method for the disconnect button. It will disconnect the client from the server.
     */
    private void disconnectClicked() {
        try {
            server.quit(callback);
            connectionState = 0;
            gui.getConnectMenuItem().setText("Connect");
            gui.getListModel().removeAllElements();
            gui.getTextAreaMainChat().append("You disconnected from the server.\n");
        } catch (Exception e) {
            System.out.println("You aint connected f00l");
        }
    }

    /**
     * Method for the quit button. It will quit the program and successfully make the server remove the client from the chat.
     */
    private void quitClicked() {
        if (connectionState == 1) {
            try {
                server.quit(callback);
                connectionState = 0;
                System.exit(0);
            } catch (Exception e) {
                System.out.println("You aint connected f00l");
            }
        } else {
            System.exit(0);
        }
    }

    /**
     * This is the method where it is listening to keyevents, in this case, if the enter button is pushed while the connectionstate is 1(connected), it will try to broadcast the message
     * afterwards, it will empty the inputText field, and will be ready for the next sentence. Note that the message
     * WILL BE sent to the server, even if its an empty string - however, the server will not broadcast empty messages
     * @param evt
     */
    private void inputTextKeyPressed(java.awt.event.KeyEvent evt) {
        int key = evt.getKeyCode();

        if (key == evt.VK_ENTER && connectionState == 1 && selectedTab == -1) {
            try {
                server.broadcast(gui.getInputText().getText().trim(), callback);
                gui.getInputText().setText("");
            } catch (RemoteException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            }
        } else if (key == evt.VK_ENTER && connectionState == 1 && selectedTab > -1 && !pmChatList.isEmpty()) {
            try {
                server.whisper(gui.getInputText().getText().trim(), pmChatList.get(selectedTab).getPmUserName(), callback);
                gui.getInputText().setText("");

            } catch (RemoteException ex) {
                Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
            }
        } else if (key == evt.VK_ENTER && connectionState == 0) {
            gui.getTextAreaMainChat().append("You are not connected to the server.\n");
            gui.getInputText().setText("");
        }
    }

    /**
     * This listener will help creating the right tab for the user you have double-clicked on.
     *
     */
    private class UserListSelectionListener implements ListSelectionListener {

        String selectedName;

        public UserListSelectionListener() {
        }

        public void valueChanged(ListSelectionEvent e) {
            selectedName = (String) gui.getUserList().getSelectedValue();
            gui.getInputText().requestFocusInWindow();
        }
    }

    /**
     * A listener for the tabs. Since we are using the same JTextField for text inputs, we need a tab listener
     * so we know who the receiver of the message is.
     */
    private class ChangeTabListener implements ChangeListener {

        public ChangeTabListener() {
        }

        public void stateChanged(ChangeEvent changeEvent) {
            JTabbedPane sourceTabbedPane = (JTabbedPane) changeEvent.getSource();
            selectedTab = sourceTabbedPane.getSelectedIndex() - 1;

        }
    }

    /**
     * A listener for the menu. It will do methods based on what menu item you are
     * clicking on.
     */
    private class MenuActionListener implements ActionListener {

        JMenuItem source;
        String text;

        public MenuActionListener() {
        }

        public void actionPerformed(ActionEvent e) {
            source = (JMenuItem) (e.getSource());
            text = source.getText();

            if (text.equals("Connect")) {
                connectClicked();
            } else if (text.equals("Disconnect")) {
                disconnectClicked();
            } else if (text.equals("Quit")) {
                quitClicked();
            }
        }
    }

    /**
     * The action listener for the mouse. If you double click on an item in the userList
     * it will check if it meets the requirements for creating a new tab.
     */

    private class ActionUserList extends MouseAdapter {

        String name;

        public ActionUserList() {
        }

        public void mouseClicked(MouseEvent e) {
            name = (String) gui.getUserList().getSelectedValue();

            try {
                if (e.getClickCount() == 2 && !callback.getUserName().equals(name) && !callback.tabAlreadyOpened(name) && !name.equals("@Martleif")) {
                    callback.createTab(name);
                }
            } catch (RemoteException ex) {
                System.out.println("Remote exception: " + ex);
            }
        }
    }

    /**
     * This is the action listener for the themes. Each theme item have been marked
     * with a parsed integer action command. And based on the commands performed it
     * will choose the right case to run.
     */
    private class ThemeActionListener implements ActionListener {

        String actionCmd;
        int nr;

        public ThemeActionListener() {
        }

        public void actionPerformed(ActionEvent e) {
            actionCmd = e.getActionCommand();
            nr = Integer.parseInt(actionCmd);
            switch (nr) {
                case 1:
                    gui.setThemeDefault();
                    break;
                case 2:
                    gui.setThemeMatrix();
                    break;
                case 3:
                    gui.setThemeGirly();
                    break;
                default:
                    System.out.println("Invalid Theme.");
            }

        }
    }

    public static void main(String[] args) throws Exception {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new RMISecurityManager());
        }
        java.awt.EventQueue.invokeLater(new Runnable() {

            public void run() {
                new Client();

            }
        });

    }
    /**
     * Class for implementing Callback
     */
    public class CallbackImpl extends UnicastRemoteObject implements Callback {

        String userName;
        JTextArea area;
        int quizScore = 0;
        JTextArea pmArea;

        /**
         * Empty constructor.
         * @throws RemoteException
         */
        public CallbackImpl() throws RemoteException {
        }

        /**
         * A simple method that formats the clients local time into a suitable
         * timestamp string.
         * @return String
         */

        public String timestamp() {
            Date todaysDate = new Date();
            SimpleDateFormat formatter = new SimpleDateFormat("[HH:mm:ss] ");
            return formatter.format(todaysDate);
        }
        /**
         * This method prints a text to the main chat whenever a user connects to the chat.
         * @param userName
         * @throws RemoteException
         */
        public void connectMessage(String userName) throws RemoteException {
            if (area != null) {
                area.append(timestamp() + userName + " has connected.\n");
                gui.getTextAreaMainChat().setCaretPosition(gui.getTextAreaMainChat().getDocument().getLength());
            }
        }

        /**
         * This method prints all the messages that are being broadcasted to the main chat.
         * @param message
         * @param userName
         * @throws RemoteException
         */
        public void broadcastMessage(String message, String userName) throws RemoteException {
            if (area != null) {
                area.append(timestamp() + userName + " wrote: " + message + "\n");
                area.setCaretPosition(area.getDocument().getLength());
            }
        }

        /**
         * This method prints the private conversations into the right text areas.
         * @param message
         * @param toName
         * @param fromName
         * @throws RemoteException
         */
        public void whisperMessage(String message, String toName, String fromName) throws RemoteException {
            if (pmArea != null && !userName.equals(fromName) && !fromName.equals("You")) {
                callback.getPmTextArea(fromName).append(timestamp() + fromName + " wrote: " + message + "\n");
                callback.getPmTextArea(fromName).setCaretPosition(callback.getPmTextArea(fromName).getDocument().getLength());
            } else if (fromName.equals("You")) {
                for (int i = 0; i < pmChatList.size(); i++) {
                    if (pmChatList.get(i).getPmUserName().equals(toName)) {
                        setPmTextArea(pmChatList.get(i).getPmTextArea());
                        pmChatList.get(i).getPmTextArea().append(timestamp() + fromName + " wrote : " + message + "\n");
                        pmChatList.get(i).getPmTextArea().setCaretPosition(pmChatList.get(i).getPmTextArea().getDocument().getLength());
                        break;
                    }
                }

            }
        }

        /**
         * This method prints whatever the bot wants to say to the main chat.
         * @param message
         * @param botName
         * @throws RemoteException
         */
        public void botMessage(String message, String botName) throws RemoteException {
            if (area != null) {
                area.append(timestamp() + "The bot " + botName + " whispers you: " + message + "\n");
                gui.getTextAreaMainChat().setCaretPosition(gui.getTextAreaMainChat().getDocument().getLength());

            }

        }
        /**
         * This method will print a text in the main chat that another user has changed his/her name.
         * If you are in a private conversation with this person then the text will also be displayed in that
         * window and the tab title for that person will be renamed.
         * @param newName
         * @param userName
         * @throws RemoteException
         */
        public void rename(String newName, String userName) throws RemoteException {
            if (area != null) {
                area.append(timestamp() + userName + " changed name to: " + newName + "\n");
                gui.getTextAreaMainChat().setCaretPosition(gui.getTextAreaMainChat().getDocument().getLength());

            }

            if (!pmChatList.isEmpty() && !this.userName.equals(userName)) {
                for (int i = 0; i < pmChatList.size(); i++) {
                    if (pmChatList.get(i).getPmUserName().equals(userName)) {
                        pmChatList.get(i).setPmUserName(newName);
                        gui.getTabbedMainChat().setTitleAt(i + 1, newName);
                        pmChatList.get(i).getPmTextArea().append(timestamp() + userName + " changed name to: " + newName + "\n");
                        pmChatList.get(i).getPmTextArea().setCaretPosition(pmChatList.get(i).getPmTextArea().getDocument().getLength());
                        break;
                    }
                }
            }

        }

        /**
         * This method will print a text in the main chat that a user has left the chat. If a client is having a private
         * conversation with that person it will also print the text into that text area.
         * @param userName
         * @throws RemoteException
         */
        public void quitMessage(String userName) throws RemoteException {
            if (area != null) {
                area.append(timestamp() + userName + " has left the chat.\n");
            }
            if (!pmChatList.isEmpty()) {
                for (int i = 0; i < pmChatList.size(); i++) {
                    if (pmChatList.get(i).getPmUserName().equals(userName)) {
                        pmChatList.get(i).getPmTextArea().append(timestamp() + userName + " has left the chat.\n");
                        pmChatList.get(i).getPmTextArea().setCaretPosition(pmChatList.get(i).getPmTextArea().getDocument().getLength());

                    }
                }
            }
        }

        /**
         * This method is used by the reciever of a whisper. It will run the createTab() method and
         * set the text area so the message will be printed on the right text area.
         * @param fromName
         * @throws RemoteException
         */
        public void receiveNewTab(String fromName) throws RemoteException {
            createTab(fromName);
            for (int i = 0; i < pmChatList.size(); i++) {
                if (pmChatList.get(i).getPmUserName().equals(fromName)) {
                    setPmTextArea(pmChatList.get(i).getPmTextArea());
                    break;
                }
            }

        }

        /**
         * This method will check if a client has a tab opened for a user. It will check
         * the array list for tabs and see if there's a tab excisting for that user.
         * It will return true if the client has a tab opened for that user, or false if not.
         * @param name
         * @return true if there is already a tab open, false otherwise
         * @throws RemoteException
         */
        public boolean tabAlreadyOpened(String name) throws RemoteException {
            boolean isThereOne = false;
            if (!pmChatList.isEmpty()) {
                for (int i = 0; i < pmChatList.size(); i++) {
                    if (pmChatList.get(i).getPmUserName().equals(name)) {
                        isThereOne = true;
                        break;
                    } else if (!pmChatList.get(i).getPmUserName().equals(name)) {
                        isThereOne = false;
                    }
                }
            }
            return isThereOne;
        }

        /**
         * This method gets the list of users. It will first be cleared and then users
         * will be added again to make sure that the clientList is always updated.
         * SwingUtilites.invokeLater() is used to make sure that the GUI updates the list
         * after all the users have been added.
         * @param userList
         * @throws RemoteException
         */
        public void getClientList(ArrayList<String> userList) throws RemoteException {
            gui.getListModel().clear();
            for (int i = 0; i < userList.size(); i++) {
                String user = userList.get(i);
                gui.getListModel().addElement(user);
                users.add(user);
            }
            SwingUtilities.invokeLater(new Runnable() {

                public void run() {
                    gui.getUserList().updateUI();
                }
            });
        }

        /**
         * This method will disconnect the client from the server. And it's being called
         * by the server when an OP uses the kick-command on the client.
         * @throws RemoteException
         */
        public void getKicked() throws RemoteException {
            new Thread() {

                public void run() {
                    System.out.print("Shutting down...");
                    try {
                        sleep(2000);
                        server.quit(callback);
                        connectionState = 0;
                        gui.getConnectMenuItem().setText("Connect");
                        gui.getListModel().removeAllElements();
                        gui.getTextAreaMainChat().append("You disconnected from the server.\n");
                    } catch (RemoteException ex) {
                        Logger.getLogger(Client.class.getName()).log(Level.SEVERE, null, ex);
                    } catch (InterruptedException e) {
                        System.out.println("UH OH :" + e);
                    }
                }
            }.start();
        }

        /**
         * This method returns the username of the callback object.
         * @return String
         * @throws RemoteException
         */
        public String getUserName() throws RemoteException {
            return userName;
        }

        /**
         * This method sets the name of the callback object.
         * @param userName
         * @throws RemoteException
         */
        public void setUserName(String userName) throws RemoteException {
            this.userName = userName;
        }

        /**
         * This method will create a tab and add it to the pmChatList. It
         * will also set the text area so messages will be printed on to the
         * right text area.
         * @param userName
         * @throws RemoteException
         */
        public void createTab(String userName) throws RemoteException {
            if (!pmChatList.isEmpty()) {
                for (int i = 0; i < pmChatList.size(); i++) {
                    if (!pmChatList.get(i).getPmUserName().equals(userName)) {
                        pmChatList.add(gui.createNewChatTab(userName));
                        setPmTextArea(getPmTextArea(userName));
                        break;
                    }
                }
            } else if (pmChatList.isEmpty()) {
                pmChatList.add(gui.createNewChatTab(userName));
                setPmTextArea(getPmTextArea(userName));
            }
        }

        /**
         * This method sets the main chat text area.
         * @param area
         * @throws RemoteException
         */
        public void setTextArea(javax.swing.JTextArea area) throws RemoteException {
            this.area = area;
        }

        /**
         * This method sets the private message text area.
         * @param pmArea
         * @throws RemoteException
         */
        public void setPmTextArea(javax.swing.JTextArea pmArea) throws RemoteException {
            this.pmArea = pmArea;
        }

        /**
         * This method will return the right text area for the wanted user.
         * @param pmTabName
         * @return JTextArea
         * @throws RemoteException
         */
        public JTextArea getPmTextArea(String pmTabName) throws RemoteException {
            for (int i = 0; i < pmChatList.size(); i++) {
                if (pmChatList.get(i).getPmUserName().equals(pmTabName)) {
                    pmArea = pmChatList.get(i).getPmTextArea();
                    break;
                }
            }
            return pmArea;
        }

        /**
         * This method will return the quiz score.
         * @return int
         * @throws RemoteException
         */
        public int getQuizScore() throws RemoteException {
            return quizScore;
        }

        /**
         * This method will increase the quiz score.
         * @throws RemoteException
         */
        public void setQuizScore() throws RemoteException {
            this.quizScore++;
        }

        /**
         * This method will reset the quiz score to zero.
         * @throws RemoteException
         */
        public void deleteQuizScore() throws RemoteException {
            quizScore = 0;
        }
    }
}
